package ro.gillardon.persistence.document;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.apache.log4j.Logger;

import ro.gillardon.commons.Price;
import ro.gillardon.commons.enums.City;
import ro.gillardon.commons.enums.Currency;
import ro.gillardon.commons.enums.DayType;
import ro.gillardon.commons.enums.Hotel;
import ro.gillardon.commons.enums.RoomType;
import ro.gillardon.commons.hotel.HotelConditions;
import ro.gillardon.commons.hotel.HotelDetailsWrapper;
import ro.gillardon.commons.logger.BTPLogger;
import ro.gillardon.persistence.HotelDetailsDAO;
import ro.gillardon.persistence.document.model.HotelDetails;
import ro.gillardon.persistence.exception.PersistenceException;

import com.orientechnologies.orient.core.db.document.ODatabaseDocumentPool;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.exception.OStorageException;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
/**
 * Concrete HotelDetailsDAO
 *
 * @author haiurar
 */
public class DocumentHotelDetailsDAO extends AbstractApi implements HotelDetailsDAO {

  /** The database connector. */
  private final ODatabaseDocumentTx db;

  private static final String SELECT_HOTEL_DETAILS =
    "SELECT * FROM HotelDetails ORDER BY date DESC where name = '%s' AND dayType = '%s' and roomType = '%s' LIMIT 1";

  private static final String SELECT_HOTELS_FROM_LOCATION =
    "SELECT name from HotelDetails WHERE location = '%s' GROUP BY name";

  private static final String SELECT_ALL_LOCATIONS = "SELECT location FROM HotelDetails GROUP BY  location";

  private static final String ERROR_MESSAGE = "Offer for %s during %s doesn't exist";
  private static final String LOCATION = "location";
  private static final String PRICE = "price";
  private static final String HOTEL_NAME = "name";
  private static final String ROOM_TYPE = "roomType";
  private static final String DAY_TYPE = "dayType";

  private static final Logger LOG = Logger.getLogger(DocumentHotelDetailsDAO.class);

  /**
   * Instantiates a new DocumentHotelDetailsDAO.
   */
  public DocumentHotelDetailsDAO() {
    this.db = ODatabaseDocumentPool.global().acquire(DATABASE_URL, USERNAME, PASSWORD);
  }

  @Override
  public HotelDetails save(final HotelDetails hotelDetails) throws PersistenceException {
    try {
      hotelDetails.getDoc().save();
      return hotelDetails;
    } catch (IllegalArgumentException | OStorageException e) {
      throw new PersistenceException(e.getMessage(), e);
    } finally {
      this.db.close();
    }
  }

  @Override
  public HotelDetailsWrapper save(final HotelDetailsWrapper hotelDetailsWrapper) throws PersistenceException {
    LocalDateTime date = LocalDateTime.now();
    Hotel hotel = hotelDetailsWrapper.getHotel();

    // @formatter:off
    saveNewHotelDetail(RoomType.SINGLE, DayType.WEEKDAY, hotel, date, () -> hotelDetailsWrapper.getSingleRoomWeekdayPrice().getAmount());
    saveNewHotelDetail(RoomType.SINGLE, DayType.WEEKEND, hotel, date, () -> hotelDetailsWrapper.getSingleRoomWeekendPrice().getAmount());
    saveNewHotelDetail(RoomType.DOUBLE, DayType.WEEKDAY, hotel, date, () -> hotelDetailsWrapper.getDoubleRoomWeekdayPrice().getAmount());
    saveNewHotelDetail(RoomType.DOUBLE, DayType.WEEKEND, hotel, date, () -> hotelDetailsWrapper.getDoubleRoomWeekendPrice().getAmount());
    // @formatter:on

    return hotelDetailsWrapper;
  }

  private void saveNewHotelDetail(final RoomType roomType, final DayType dayType, final Hotel hotel,
    final LocalDateTime date, final Supplier<Double> supplier) throws PersistenceException {
    HotelDetails detailToBeSaved = new HotelDetails(roomType, dayType, hotel.getCity(), hotel, date, supplier.get());
    save(detailToBeSaved);
  }

  @Override
  public HotelDetailsWrapper getLatestHotelOffer(final Hotel hotel) throws PersistenceException {

    try {
      HotelDetailsWrapper hdw = new HotelDetailsWrapper(hotel);
      setPriceValues(hotel, hdw);
      return hdw;
    } catch (IllegalArgumentException e) {
      throw new PersistenceException(e.getMessage(), e);
    } finally {
      this.db.close();
    }
  }

  private void setPriceValues(final Hotel hotel, final HotelDetailsWrapper hdw) throws PersistenceException {
    List<ODocument> singleRoomWeekend = executeQuery(DayType.WEEKEND, RoomType.SINGLE, hotel);
    List<ODocument> doubleRoomWeekend = executeQuery(DayType.WEEKEND, RoomType.DOUBLE, hotel);
    List<ODocument> singleRoomWeekday = executeQuery(DayType.WEEKDAY, RoomType.SINGLE, hotel);
    List<ODocument> doubleRoomWeekday = executeQuery(DayType.WEEKDAY, RoomType.DOUBLE, hotel);

    //@formatter:off
    executeSetter(value -> hdw.setSingleRoomWeekendPrice(new Price(value,Currency.EUR)), singleRoomWeekend);
    executeSetter(value -> hdw.setDoubleRoomWeekendPrice(new Price(value,Currency.EUR)), doubleRoomWeekend);
    executeSetter(value -> hdw.setSingleRoomWeekdayPrice(new Price(value,Currency.EUR)), singleRoomWeekday);
    executeSetter(value -> hdw.setDoubleRoomWeekdayPrice(new Price(value,Currency.EUR)), doubleRoomWeekday);
    //@formatter:on
  }

  private void executeSetter(final Consumer<Double> setterFunction,
    final List<ODocument> collectionThatHoldsTheLatestValue) throws PersistenceException {
    if (collectionThatHoldsTheLatestValue.isEmpty()) {
      String roomType = collectionThatHoldsTheLatestValue.get(0).field(ROOM_TYPE);
      String dayType = collectionThatHoldsTheLatestValue.get(0).field(DAY_TYPE);
      BTPLogger.error(LOG, String.format(ERROR_MESSAGE, roomType, dayType));
      throw new PersistenceException();
    } else {
      setterFunction.accept(convertFieldToDouble(collectionThatHoldsTheLatestValue, PRICE));
    }
  }

  private double convertFieldToDouble(final List<ODocument> singleRoomWeekend, final String fieldName) {
    return Double.parseDouble(singleRoomWeekend.get(0).field(fieldName).toString());
  }

  private List<ODocument> executeQuery(final DayType dayType, final RoomType roomType, final Hotel hotel) {
    List<ODocument> hotelDetails =
      this.db.query(createQuery(SELECT_HOTEL_DETAILS, hotel, dayType, roomType));
    return hotelDetails;
  }

  private OSQLSynchQuery<ODocument> createQuery(final String query, final Object... params) {
    return new OSQLSynchQuery<ODocument>(String.format(query, params));
  }

  @Override
  public List<Hotel> getAllHotelsFromLocation(final City location) throws PersistenceException {
    try {
      List<ODocument> hotelsFromLocation =
        this.db.query(createQuery(SELECT_HOTELS_FROM_LOCATION, location));

      List<Hotel> results = hotelsFromLocation.stream().collect(() -> new ArrayList<>(),
        (c, e) -> c.add(Hotel.valueOf(e.field(HOTEL_NAME))),
        (c1, c2) -> c1.addAll(c2));

      return results;
    } catch (IllegalArgumentException e) {
      throw new PersistenceException(e.getMessage(), e);
    } finally {
      this.db.close();
    }
  }

  @Deprecated
  @Override
  /**
   * @deprecated (not yet implemented)
   */
  public HotelDetails getLastHotelOffer(final HotelConditions hotelCondition) throws PersistenceException {
    // TODO Auto-generated method stub
    return null;
  }

  @Override
  public List<City> getAllLocations() throws PersistenceException {
    try {
      List<ODocument> locations =
        this.db.query(createQuery(SELECT_ALL_LOCATIONS));

      List<City> results = locations.stream().collect(() -> new ArrayList<>(),
        (c, e) -> c.add(City.valueOf(e.field(LOCATION).toString())),
        (c1, c2) -> c1.addAll(c2));
      return results;
    } catch (IllegalArgumentException e) {
      throw new PersistenceException(e.getMessage(), e);
    }
  }
}
